﻿using System;
using System.Collections.Generic;
using System.ServiceModel;
using CoreEx.Common.Data;
using LinFu.IoC.Factories;
using LinFu.IoC.Interfaces;

namespace CoreEx.ServiceModel
{
    /// <summary>
    /// A factory class that ensures that services instances are scoped by the current <see cref="OperationContext"/>.    
    /// </summary>
    /// <remarks>
    /// If a <see cref="OperationContext"/> is not available, the services are thread scoped.
    /// </remarks>
    public abstract class PerOperationFactoryBase<T> : IFactory<T> 
    {
        private static readonly IDictionary<OperationContext, IDictionary<SurrogateKey, T>> _operationScopedObjects =
            new Dictionary<OperationContext, IDictionary<SurrogateKey, T>>();

        private readonly OncePerThreadFactory<T> _oncePerThreadFactory;


        protected PerOperationFactoryBase()
        {
            _oncePerThreadFactory = new OncePerThreadFactory<T>(r => CreateInstance());
        }

        public abstract T CreateInstance();
        
        /// <summary>
        /// Creates a <see cref="OperationContext"/> scoped service instance.
        /// </summary>
        /// <param name="request">Contains information about the factory request.</param>
        /// <returns></returns>
        public T CreateInstance(IFactoryRequest request)
        {
            
            
            var currentOperationContext = OperationContext.Current;
            if (currentOperationContext == null)
                return _oncePerThreadFactory.CreateInstance(request);


            IDictionary<SurrogateKey, T> cachedObjects;
            T instance;

            if (!_operationScopedObjects.TryGetValue(currentOperationContext, out cachedObjects))
            {
                // We don't have a list for this operation context , so create a new one
                cachedObjects = new Dictionary<SurrogateKey, T>();
                _operationScopedObjects.Add(currentOperationContext, cachedObjects);                
                
                //Register av event handler so that we know when the operation completes.
                currentOperationContext.OperationCompleted += OnOperationCompleted;
            }

            var instanceKey = new SurrogateKey(new object[]{request.ServiceType,request.ServiceName});

            if (!cachedObjects.TryGetValue(instanceKey, out instance))
            {
                instance = CreateInstance();
                cachedObjects.Add(instanceKey, instance);
            }

            return instance;
        }

        static void OnOperationCompleted(object sender, EventArgs e)
        {
            IDictionary<SurrogateKey, T> cachedObjects;

            lock (_operationScopedObjects)
            {
                if (!_operationScopedObjects.TryGetValue((OperationContext)sender, out cachedObjects))
                {
                    // we don't know about this operation context (odd).
                    return;
                }
                ((OperationContext) sender).OperationCompleted -= OnOperationCompleted;
                _operationScopedObjects.Remove((OperationContext)sender);
            }


            lock(cachedObjects)
            {
                foreach (object cachedObject in cachedObjects.Values)
                {
                    if (cachedObject is IDisposable)
                        ((IDisposable)cachedObject).Dispose();
                }
            }
        }
    }
}
